import { ref, shallowRef } from 'vue'
import { isFunction, clone } from 'lodash-es'
import type { Options, Service, Fetch, FunctionContext, PluginType, EmitResults, StatusType, StateBindParams } from './types'
import type { UnWrapRefObject } from './types/utils'

export const resolvedPromise = () => new Promise<any>(() => {})

const setStateBind = <T extends StateBindParams>(oldState: T) => {
  return (newState: Partial<UnWrapRefObject<StateBindParams>>) => {
    Object.keys(newState).forEach(key => {
      if (oldState[key]) {
        oldState[key].value = newState[key]
      }
    })
  }
}

const composeMiddleware = <Data, P extends unknown[]>(middleArray: PluginType<Data, P>['onFetch'][], hook: any) => {
  return () => {
    let next: () => Promise<Data> = hook
    for (let i = middleArray.length - 1; i >= 0; i--) {
      next = middleArray[i](next)
    }
    return next()
  }
}

const createFetch = <Data, P extends unknown[]>(service: Service<Data, P>, config: Options<Data, P>): Fetch<Data, P> => {
  const { onSuccess, onError, onBefore, onAfter } = config

  const loading = ref(false)
  const data = shallowRef<Data>()
  const error = shallowRef()
  const params = ref<P>()
  const plugins = ref<Partial<PluginType<Data, P>>[]>([])
  const status = shallowRef<StatusType>('pending')

  const context = {} as FunctionContext<Data, P>

  const setState = setStateBind({ loading, data, error, params, status })

  const emit = (event: keyof PluginType<Data, P>, ...args: any[]): EmitResults<Data, P> => {
    if (event === 'onFetch') {
      const fetchFns = plugins.value.map(plugin => plugin.onFetch).filter(Boolean)
      return { servicePromise: composeMiddleware(fetchFns, args[0])() }
    } else {
      // @ts-ignore
      const res = plugins.value.map(plugin => plugin[event]?.(...args))
      return Object.assign({}, ...res)
    }
  }

  const count = ref(0)

  context.runAsync = async (...args: P) => {
    // status.value = 'pending'
    setState({ status: 'pending' })

    count.value += 1
    const currentCount = count.value

    const { isBreak = false, isReturn = false, ...rest } = emit('onBefore', args)

    if (isBreak) {
      setState({ status: 'settled' })
      return resolvedPromise()
    }

    setState({
      loading: true,
      params: args,
      ...rest
    })

    if (isReturn) {
      setState({ status: 'settled', loading: false })
      return data.value
    }

    onBefore?.(args)

    try {
      const serviceWrapper = () => new Promise<Data>(resolve => resolve(service(...params.value)))

      let { servicePromise } = emit('onFetch', serviceWrapper)

      if (!servicePromise) {
        servicePromise = serviceWrapper()
      }

      // const res = await serviceWrapper()
      const res = await servicePromise

      if (currentCount !== count.value) return resolvedPromise()

      setState({
        data: res,
        loading: false,
        error: undefined,
        status: 'settled'
      })

      emit('onSuccess', res, args)
      onSuccess?.(res, args)
      if (currentCount === count.value) {
        emit('onAfter', args, res, undefined)
      }
      onAfter?.(args)

      return res
    } catch (err: any) {
      if (currentCount !== count.value) return resolvedPromise()

      setState({
        loading: false,
        error: err,
        status: 'settled'
      })

      emit('onError', err, args)
      onError?.(err, args)

      if (currentCount === count.value) {
        emit('onAfter', args, undefined, err)
      }
      onAfter?.(args)

      throw error
    }
  }

  context.run = (...args: P) => {
    context.runAsync(...args).catch(error => {
      if (!onError) {
        console.error(error)
      }
    })
  }

  context.cancel = () => {
    count.value += 1
    setState({ loading: false })

    emit('onCancel')
  }

  context.refresh = () => {
    context.run(...(params.value || ([] as P)))
  }

  context.refreshAsync = () => {
    return context.runAsync(...(params.value || ([] as P)))
  }

  context.mutate = (x: Data | ((oldData: Data) => Data)) => {
    const mutateData = isFunction(x) ? x(data.value) : x
    const _mutateData = clone(mutateData)

    setState({
      data: _mutateData
    })

    emit('onMutate', _mutateData)
  }

  return {
    status,
    loading,
    data,
    error,
    params,
    plugins,
    context
  }
}

export default createFetch
